﻿using System;
using System.IO;
using System.Threading;
using Db4objects.Db4o;
using Db4objects.Db4o.CS.Internal;
using Db4objects.Db4o.Diagnostic;
using System.Timers;
using Db4objects.Db4o.CS;
using Db4objects.Db4o.Events;
using Db4objects.Db4o.IO;
using NLog;

namespace ZOIL.DataBackend.Server
{

    /// <summary>
    /// Represents an instance of a DB4O Server. Every instance runs exactly one database.
    /// </summary>
    public class DB4OServer
    {
        #region Logger

        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

        #endregion

        #region Private Fields

        /// <summary>
        /// The Thread that runs the server asynchronously
        /// </summary>
        private Thread _serverThread;

        private System.Timers.Timer _deleteTimer;
        
        /// <summary>
        /// indicates if we should stop the server thread 
        /// </summary>
        private bool _stopThread;

        /// <summary>
        /// The current database configuration the server should run with
        /// </summary>
        private DatabaseConfiguration _config;

        /// <summary>
        /// A dummy object which is used to lock the section of code running in the server Thread
        /// </summary>
        private readonly object _lock = new object();

        #endregion

        #region Public Properties

        public IObjectServer Db4oServer { get; private set; }

        public IEventRegistry EventRegistry { get; private set; }

        public IObjectContainer Objects { get; private set; }

        #endregion

        #region Private Methods

        /// <summary>
        /// Resets the Server Thread. This must be called when the thread has completed, cause running a thread twice is not possible.
        /// </summary>
        private void ResetThread()
        {
            var threadStart = new ThreadStart(RunServer);
            _serverThread = new Thread(threadStart);
        }

        private void ResetDeleteTimer()
        {
            _deleteTimer = new System.Timers.Timer();
            _deleteTimer.Elapsed += DeleteTimerElapsed;
        }

        void DeleteTimerElapsed(object sender, ElapsedEventArgs e)
        {
            lock(_lock)
            {
                //deleteObjects();
                var srv = Db4oServer as ObjectServerImpl;
                var ienum = srv.IterateDispatchers();
                while (ienum.MoveNext())
                {
                    var disp = (IServerMessageDispatcher)ienum.Current;
                    Logger.Debug(disp.Name);
                }
            }
        }

        #endregion

        #region Starting and Stopping the Server

        //gets called when the event registry was assigned
        public event EventHandler EventRegistryAssigned;

        private void StoreMemoryStorageToFile(MemoryStorage storage)
        {
            var origBin = storage.Bin(_config.DatabaseFileWithPath);
            var data = origBin.Data();

            var file = new Sharpen.IO.RandomAccessFile(_config.DatabaseFileWithPath, "rw");

            file.Write(data);
            file.Close();
        }

        private MemoryStorage ReadMemoryStorageFromFile()
        {
            //open and close the db, to be sure the database is created
            IObjectContainer origDb = Db4oEmbedded.OpenFile(Db4oEmbedded.NewConfiguration(), _config.DatabaseFileWithPath);
            origDb.Close();
            
            var raf = new Sharpen.IO.RandomAccessFile(_config.DatabaseFileWithPath, "r");
            var len = (int)raf.Length();
            var b = new byte[len];
            raf.Read(b, 0, len);
            raf.Close();
            var bin = new MemoryBin(b, new ConstantGrowthStrategy(5000000)); //grow by 5mb if db is full
            var storage = new MemoryStorage();
            storage.Bin(_config.DatabaseFileWithPath, bin);
            return storage;
        }

        /// <summary>
        /// Starts the Server thread
        /// </summary>
        /// <param name="c"></param>
        public void Start(DatabaseConfiguration c)
        {
            //first we need to reset the thread, since it could have been used before
            ResetThread();
            ResetDeleteTimer();
            _stopThread = false;
            _config = c;
            _serverThread.Start();
        }

        /// <summary>
        /// Stops the Server Thread
        /// </summary>
        public void Stop()
        {
            lock (_lock)
            {
                _stopThread = true;
                //Inform all waiting threads that some value has changed - here it is the _stopThread value
                Monitor.PulseAll(_lock);
            }
        }

        /// <summary>
        /// This should run in a separate thread. It opens the server and waits until it is called to close the server.
        /// </summary>
        private void RunServer()
        {
            //get a lock for the next lines of code, so nobody can change anything when we need it, except for someone who also gets
            //a lock on the same object and then does Monitor.PulseAll(locker)
            lock (_lock)
            {
                //apply the config to the global configuration scope, so we don't need to pass it to the server
                //Db4oConfig.ApplyConfig();
                
                //Try to open the server. It can be that the database file is locked, due to another process having a handle on it.
                //TODO: Write exception handling code, that is consumable in the user interface (tricky, because of threading)
                var config = Db4oConfig.GetServerConfiguration();
                if (_config.UseMemoryIOAdapter)
                {
                    var storage = ReadMemoryStorageFromFile();

                    config.File.Storage = storage;
                }

                Db4oServer = Db4oClientServer.OpenServer(config, _config.DatabaseFileWithPath, _config.Port);
                //Db4oServer = Db4oFactory.OpenServer(_config.DatabaseFile, _config.Port);

                //Grant access to every user that has been added to the config
                foreach (var u in _config.Users)
                {
                    Db4oServer.GrantAccess(u.Username, u.Password);
                }

                Objects = Db4oServer.Ext().ObjectContainer();
                EventRegistry = EventRegistryFactory.ForObjectContainer(Objects);

                if(EventRegistryAssigned != null)
                {
                    EventRegistryAssigned(this, new EventArgs());
                }

                _deleteTimer.Interval = _config.DeleteInterval;
                _deleteTimer.Start();

                //this ensures that the thread runs forever at no CPU cost (compared to a Thread.Sleep statement)
                //when the value of _stopThread changes we can go on to closing the Server
                while (!_stopThread)
                {
                    //we wait until some other thread informs us that a value has changed
                    Monitor.Wait(_lock);
                }

                if(_config.UseMemoryIOAdapter)
                {
                    var storage = config.File.Storage as MemoryStorage;
                    StoreMemoryStorageToFile(storage);
                }

                _deleteTimer.Stop();

                //finally we can close the server
                Db4oServer.Close();
            }
        }

        #endregion

        /// <summary>
        /// A class that prints out Messages related to TransparentActivation of DB4O
        /// </summary>
        private class TADiagnostics : IDiagnosticListener
        {
            public TADiagnostics()
            {
                var fs = new FileStream("out.txt", FileMode.Create);
                var sw = new StreamWriter(fs);
                Console.SetOut(sw);
            }

            public void OnDiagnostic(IDiagnostic diagnostic)
            {
                Console.WriteLine(diagnostic.ToString());
            }
        }
    }

}
